package main;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;

import javax.swing.*;

import altserial.CodeDownloader;

import serial.SerialException;
import serial.SerialInterface;
import serial.SerialReaderListener;

import gui.*;

@SuppressWarnings("serial")
public class SerialFrame extends JFrame {
	private String portName;
	private SerialInterface serialIface;
	private SendPanel sendPanel;
	private HistoryTextField lineInput;
	private JTabbedPane programTabs;
	private ArrayList<UndoableTextArea> programs;
	private ArrayList<File> programFiles;
	
	private CodeDownloader downloader;

	private Quitter q;
	private JMenuItem newFile, open, save, saveAs, close, exit;
	private JMenuItem sendProgram, sendWithExit, setPortName, testConnection, closeConnection, showHideOutput, altSender;
	private JMenuItem undo, redo;
	private JMenuItem info;
	private JFileChooser luaChooser;
	
	private OutputFrame outputWindow;

	private final static Font COURIER = new Font("Courier", Font.PLAIN, 12);
	private final static String PRELUDE_PATH = "MyPrelude.lua";
	private final static String STANDALONE_PATH = "Standalone.lua";
	public static final String LOG_PATH = ".pbllog";
	// public static final String LOG_PATH = "C:\\cygwin\\home\\burch\\log.txt";
	private final static String PORT_PATH = ".portNames";
	private final static String PROGRAM_TITLE = "pbLua Controller";
	private final static double VERSION = 0.14;
	
	public SerialFrame() throws SerialException, IOException {
		portName = null;
		outputWindow = new OutputFrame();
		serialIface = new SerialInterface(new ReaderDumper(), outputWindow);
		downloader = new CodeDownloader(getClass().getName());
		luaChooser = new LuaChooser();
		setupFrame();
		setupControls();
		setupMenus();
		setupPastPorts();
	}
	
	private UndoableTextArea program() {
		return programs.get(progIndex());
	}
	
	private int progIndex() {
		return programTabs.getSelectedIndex();
	}
	
	private void setCurrentTitle(String title) {
		programTabs.setTitleAt(programTabs.getSelectedIndex(), title);
	}

	private void setupFrame() {
		setSize(800, 400);
		setTitle(PROGRAM_TITLE);
		setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
		q = new Quitter();
		addWindowListener(q);
	}

	private void setupControls() {
		sendPanel = new SendPanel(this, serialIface);
		
		lineInput = new HistoryTextField(30);
		lineInput.setFont(COURIER);
		
		JPanel lineInputPanel = new JPanel();
		lineInputPanel.setLayout(new BorderLayout());
		lineInputPanel.add(lineInput, BorderLayout.CENTER);
		lineInputPanel.setBorder(BorderFactory.createTitledBorder("Command Line"));

		lineInput.addActionListener(new WriterSender());

		JPanel areaPanel = new JPanel();
		areaPanel.setLayout(new GridLayout(1, 1));
		
		programTabs = new JTabbedPane();
		programs = new ArrayList<UndoableTextArea>();
		programFiles = new ArrayList<File>();
		addNewTab();
		programTabs.setBorder(BorderFactory.createTitledBorder("Programs"));
		areaPanel.add(programTabs);
		
		getContentPane().setLayout(new BorderLayout());
		getContentPane().add(lineInputPanel, BorderLayout.NORTH);
		getContentPane().add(areaPanel, BorderLayout.CENTER);
		getContentPane().add(sendPanel, BorderLayout.SOUTH);
	}
	
	private int numPrograms() {
		return programs.size();
	}
	
	private int maxProgIndex() {
		return numPrograms() - 1;
	}
	
	private void addNewTab() {
		programs.add(new UndoableTextArea());
		programTabs.add("New", createTextPanel(programs.get(maxProgIndex())));		
		programTabs.setSelectedIndex(maxProgIndex());
		programFiles.add(null);
	}

	private JPanel createTextPanel(JTextArea area) {
		area.setFont(COURIER);
		JPanel areaPanel = new JPanel();
		areaPanel.setLayout(new BorderLayout());
		areaPanel.add(new JScrollPane(area), BorderLayout.CENTER);
		return areaPanel;
	}

	private void setupMenus() {
		JMenuBar bar = new JMenuBar();
		JMenu fileMenu = new JMenu("File");
		bar.add(fileMenu);

		newFile = new JMenuItem("New");
		newFile.addActionListener(new Newer());
		fileMenu.add(newFile);
		//newFile.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_DOWN_MASK));

		Opener opener = new Opener();

		open = new JMenuItem("Open");
		open.addActionListener(opener);
		fileMenu.add(open);
		open.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK));
		
		close = new JMenuItem("Close");
		close.addActionListener(new Closer());
		fileMenu.add(close);
		//close.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.CTRL_DOWN_MASK));
		
		Saver saver = new Saver();

		save = new JMenuItem("Save");
		save.addActionListener(saver);
		fileMenu.add(save);
		save.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK));

		saveAs = new JMenuItem("Save As...");
		saveAs.addActionListener(saver);
		fileMenu.add(saveAs);
		
		fileMenu.addSeparator();
		
		exit = new JMenuItem("Exit");
		exit.addActionListener(q);
		fileMenu.add(exit);
		
		JMenu editMenu = new JMenu("Edit");
		bar.add(editMenu);
		
		undo = new JMenuItem("Undo");
		undo.addActionListener(new Undoer());
		editMenu.add(undo);
		undo.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.CTRL_DOWN_MASK));
		
		redo = new JMenuItem("Redo");
		redo.addActionListener(new Redoer());
		editMenu.add(redo);
		redo.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y, InputEvent.CTRL_DOWN_MASK));
		
		JMenu robotMenu = new JMenu("NXT");
		bar.add(robotMenu);
		
		sendProgram = new JMenuItem("Send program");
		sendProgram.addActionListener(new ProgramSender());
		robotMenu.add(sendProgram);
		
		sendWithExit = new JMenuItem("Send and unplug");
		sendWithExit.addActionListener(new Unplugger());
		robotMenu.add(sendWithExit);
		
		robotMenu.addSeparator();
		
		setPortName = new JMenuItem("Configure serial port");
		setPortName.addActionListener(new SerialPortSetter());
		robotMenu.add(setPortName);
		/*
		testConnection = new JMenuItem("Test connection");
		testConnection.addActionListener(new ConnectionTester());
		robotMenu.add(testConnection);
		
		closeConnection = new JMenuItem("Close connection");
		closeConnection.addActionListener(new ConnectionCloser());
		robotMenu.add(closeConnection);
		*/
		robotMenu.addSeparator();
		
		showHideOutput = new JMenuItem("Show output");
		showHideOutput.addActionListener(new OutputShower());
		robotMenu.add(showHideOutput);
		/*
		robotMenu.addSeparator();
		
		altSender = new JMenuItem("Alternative downloader");
		altSender.addActionListener(new AltSender());
		robotMenu.add(altSender);
		*/
		JMenu helpMenu = new JMenu("Help");
		bar.add(helpMenu);
		
		info = new JMenuItem("Version");
		info.addActionListener(new Abouter());
		helpMenu.add(info);	

		setJMenuBar(bar);
	}
	
	private class AltSender implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			try {
				downloader.sendCode(program().getText() + "\n");
			} catch (IOException ioe) {
				ioe.printStackTrace();
			}
		}
	}
	
	private class Newer implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			addNewTab();
		}
	}

	private class ReaderDumper implements SerialReaderListener {
		public void stringReceived(String input) {/*
			if (output == null) {
				System.out.println("output: " + input);
			} else {
				output.setText(output.getText() + input);
			}
		*/}

		public void unplugged() {
			JOptionPane.showMessageDialog(null, "NXT Unplugged; exiting...");
			quit();
		}

		public void halted() {
			JOptionPane.showMessageDialog(null, "Connection closed");
		}		
	}
	
	private class Undoer implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			program().undo();
		}
	}
	
	private class Redoer implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			program().redo();
		}
	}
	
	private class Closer implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			save(false, true);
			int target = progIndex();
			programTabs.remove(target);
			programs.remove(target);
		}
	}
	
	private class OutputShower implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			outputWindow.setVisible(true);
		}
	}
	
	private class SerialPortSetter implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			setSerialPortName();
		}
	}
	
	private void setSerialPortName() {
		String value = JOptionPane.showInputDialog("Enter serial port name:");
		if (value != null) {
			if (value.equals("")) {
				portName = null;
			} else {
				portName = value;
				downloader.addPossiblePort(value);
			}
		}
	}
	
	private boolean sendCommand(String cmd, boolean fromUser, String descript, boolean disconnect) {
		try {
			if (!serialIface.isConnected()) {
				portName = serialIface.setup(portName, getClass().getName());
				serialIface.addPossibleName(portName);
				sendPrelude();
			}
			return sendPanel.send(cmd, descript, fromUser, disconnect);
		} catch (SerialException e) {
			if (e.isNoSuchPortException()) {
				if (portName == null) {
					sendPanel.setMessage("No serial port name specified");
				} else {
					sendPanel.setMessage("Serial port \"" + portName + "\" does not exist");
				}
			} else {
				e.printStackTrace();
				sendPanel.setMessage( "Unable to connect to NXT");
			}
			return false;
		} catch (Exception e) {
			e.printStackTrace();
			sendPanel.setMessage("Unable to connect to NXT");
			return false;
		}
	}
	
	private void sendPrelude() {
		String text = slurpFile(PRELUDE_PATH);
		sendPanel.send(text, PRELUDE_PATH, false, false);
		/*
		InputStream stream = getClass().getResourceAsStream(PRELUDE_PATH);
		String text = slurpFile(stream, "prelude");
		sendPanel.send(text, "prelude", false, false);
		*/
	}

	private class WriterSender implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			sendSingleCommand(lineInput.getText());
		}
	}
	
	private void sendSingleCommand(String cmd) {
		boolean sent = sendCommand(cmd + "\n", true, "command", false);
		if (sent) {
			lineInput.archiveAndClear();
		}
	}

	private class ProgramSender implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			sendProgram(false);
		}
	}
	
	private class Unplugger implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			sendProgram(true);
		}
	}
	
	public void sendProgram(boolean unplug) {
		if (unplug) {
			saveAll();
			
			String standalone = "function __main()\n";
			standalone += "cls()\n";
			standalone += program().getText();
			standalone += "\nend\n";
			
			standalone += slurpFile(STANDALONE_PATH);
			sendCommand(standalone, false, "program", true);
		} else {
			sendCommand("resetAllMotors()\n" + program().getText() + "\n", true, "program", false);
			//sendCommand(program().getText() + "\n", true, "program");
		}
	}

	private class Quitter extends WindowAdapter implements ActionListener {
		public void windowClosing(WindowEvent arg0) {
			quit();
		}

		public void actionPerformed(ActionEvent e) {
			quit();
		}
	}

	private void quit() {
		saveAll();
		savePortNames();
		System.exit(0);
	}
	
	private class Abouter implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			JOptionPane.showMessageDialog(null, PROGRAM_TITLE + " " + VERSION + " (Maintainer: Gabriel Ferrer; ferrer@hendrix.edu)");
		}
	}

	private class Opener implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			int option = luaChooser.showOpenDialog(SerialFrame.this);
			if (option == JFileChooser.APPROVE_OPTION) {
				File choice = luaChooser.getSelectedFile();
				addNewTab();
				try {
					program().open(choice);
					setFileForTab(choice);
				} catch (FileNotFoundException e1) {
					JOptionPane.showMessageDialog(null, "Problem opening " + choice.getName());
				}
			}
		}
	}
	
	private void setFileForTab(File choice) {
		setCurrentTitle(choice.getName());
		programFiles.set(progIndex(), choice);
	}
	
	private class Saver implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			save(e.getSource() == saveAs, false);
		}
	}
	
	private void saveAll() {
		int current = programTabs.getSelectedIndex();
		save(false, true);
		for (int i = 0; i < programTabs.getTabCount(); ++i) {
			if (i != current) {
				programTabs.setSelectedIndex(i);
				save(false, true);
			}
		}
		programTabs.setSelectedIndex(current);
	}

	private void save(boolean savingAs, boolean askFirst) {
		File choice = programFiles.get(progIndex());
		boolean saving = savingAs || program().needsSaving();
		if (saving && askFirst) {
			String title = programTabs.getTitleAt(progIndex());
			int result = JOptionPane.showConfirmDialog(null, "Save changes to \"" + title + "\"?", "Save changes?", JOptionPane.YES_NO_OPTION);
			saving = result == JOptionPane.YES_OPTION;
		}
		
		if (!saving) {return;}
		
		try {
			if (savingAs || choice == null) {
				// This solution did not quite work.  Hence, I resorted to the hack that follows.
				//luaChooser.setSelectedFile(choice == null ? luaChooser.getCurrentDirectory() : choice);
				if (choice == null) {
					choice = new File(luaChooser.getCurrentDirectory(), "New");
				}
				luaChooser.setSelectedFile(choice);
				
				int option = luaChooser.showSaveDialog(this);
				if (option == JFileChooser.APPROVE_OPTION) {
					choice = LuaChooser.withValidSuffix(luaChooser.getSelectedFile());
					program().save(choice);
					setFileForTab(choice);
				}
			} else {
				program().save(choice);
			}

		} catch (IOException ioe) {
			JOptionPane.showMessageDialog(null, choice == null ? "Cannot designate file for saving" : "Problem saving " + choice.getName());
		}
	}
	
	private String slurpFile(String name) {
		InputStream stream = getClass().getResourceAsStream(name);
		StringBuilder text = new StringBuilder();
		InputStreamReader in = null;
		try {
			in = new InputStreamReader(stream);
			char[] buf = new char[128];
			while (true) {
				int nchars = in.read(buf);
				if (nchars <= 0) break;
				text.append(buf, 0, nchars);
			}
			return text.toString();
		} catch (IOException e) {
			JOptionPane.showMessageDialog(null, "Problem loading " + name);
			return "";
		} finally {
			if (in != null) {
				try {
					in.close();
				} catch (IOException e) { }
			}
		}
	}

	private void setupPastPorts() {
		try {
			serialIface.loadPossibleNamesFrom(PORT_PATH);
		} catch (FileNotFoundException e) {
			System.out.println("Port file does not exist for reading");
		}
		/*
		portFile = new File(PORT_PATH);
		try {
			Scanner s = new Scanner(portFile);
			while (s.hasNextLine()) {
				String name = s.nextLine();
				serialIface.addPossibleName(name);
				downloader.addPossiblePort(name);
			}
			s.close();
		} catch (FileNotFoundException e) {
			System.out.println("Port file does not exist for reading");
		}
		*/
	}

	private void savePortNames() {
		try {
			PrintWriter portOut = new PrintWriter(new FileWriter(new File(PORT_PATH)));
			for (String port : serialIface.getPossibleNames()) {
				portOut.println(port);
			}
			portOut.close();
		} catch (IOException fnfe) {
			System.out.println("Could not create port file");
		}
	}
	
	private class ConnectionTester implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			sendSingleCommand("cls(); write(\"Connection live\")");
		}
	}
	
	private class ConnectionCloser implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			serialIface.closeConnection();
		}	
	}

	public static void main(String[] args) {
		try {
			new SerialFrame().setVisible(true);
		} catch (Exception e) {
			e.printStackTrace();
			System.exit(1);
		}
	}
}

